#include "skia_framework.h"

#include <chrono>
#include <cstring>
#include <functional>
#include <iomanip>
#include <sstream>

#include <include/core/SkBitmap.h>
#include <include/core/SkCanvas.h>
#include <include/core/SkFontMgr.h>
#include <include/core/SkPath.h>
#include <include/core/SkTextBlob.h>
#include <include/core/SkFontMetrics.h>
#include <SDL2/SDL.h>

#include "iskia_control.h"

using namespace std::chrono_literals;

struct SDLData {
    SDL_Window *window_ = nullptr;
    SDL_Renderer *renderer_ = nullptr;
    SDL_Surface *surface_ = nullptr;
    SDL_Texture *texture_ = nullptr;
    SDL_Rect rect_ = {};
};

SkiaFramework::SkiaFramework()
{
    data_ = new struct SDLData;
}

SkiaFramework::~SkiaFramework()
{
    if (uiThread_) {
        uiThread_->join();
    }

    auto &data = *reinterpret_cast<struct SDLData *>(data_);
    if (data.surface_) {
        SDL_FreeSurface(data.surface_);
    }

    if (data.texture_) {
        SDL_DestroyTexture(data.texture_);
    }

    if (data.renderer_) {
        SDL_DestroyRenderer(data.renderer_);
    }

    if (data.window_) {
        SDL_DestroyWindow(data.window_);
    }

    SDL_Quit();
}

void SkiaFramework::SetDrawFunc(const DrawFunc &onDraw)
{
    onDraw_ = onDraw;
}

void SkiaFramework::SetDraw2Func(const Draw2Func &onDraw)
{
    onDraw2_ = onDraw;
}

void SkiaFramework::SetResizeFunc(const ResizeFunc &onResize)
{
    onResize_ = onResize;
}

void SkiaFramework::SetGPUAllowance(bool allow)
{
    allowGPU_ = allow;
}

void SkiaFramework::SetWindowWidth(int width)
{
    windowWidth_ = width;
}

void SkiaFramework::SetWindowHeight(int height)
{
    windowHeight_ = height;
}

void SkiaFramework::SetWindowTitle(const std::string &title)
{
    windowTitle_ = title;
}

int SkiaFramework::GetWindowWidth() const
{
    auto &data = *reinterpret_cast<struct SDLData *>(data_);
    int w;
    SDL_GetWindowSize(data.window_, &w, nullptr);
    return w;
}

int SkiaFramework::GetWindowHeight() const
{
    auto &data = *reinterpret_cast<struct SDLData *>(data_);
    int h;
    SDL_GetWindowSize(data.window_, nullptr, &h);
    return h;
}

void SkiaFramework::Run()
{
    auto &data = *reinterpret_cast<struct SDLData *>(data_);
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
    constexpr auto centered = SDL_WINDOWPOS_CENTERED;
    constexpr auto flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN;
    data.window_ = SDL_CreateWindow(windowTitle_.c_str(),
                               centered, centered,
                               windowWidth_, windowHeight_,
                               flags);
    SDL_RendererFlags rendererFlag = allowGPU_ ? SDL_RENDERER_ACCELERATED : SDL_RENDERER_SOFTWARE;
    rendererFlag = SDL_RENDERER_SOFTWARE;
    data.renderer_ = SDL_CreateRenderer(data.window_, -1, rendererFlag);
    SDL_RenderClear(data.renderer_);
    data.rect_ = { 0, 0, windowWidth_, windowHeight_ };

    uiThreadRunning_ = true;
    uiThread_ = std::make_unique<std::thread>(std::bind(&SkiaFramework::UIThreadMain, this));

    while (uiThreadRunning_) {
        SDL_Event e = {};
        while (SDL_PollEvent(&e)) {
            std::lock_guard<std::mutex> lock(propsMutex_);
            if (e.type == SDL_QUIT) {
                uiThreadRunning_ = false;
            }

            if (e.type == SDL_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
                SDL_GetWindowSize(data.window_, &data.rect_.w, &data.rect_.h);
                if (onResize_) {
                    onResize_(data.rect_.w, data.rect_.h);
                }
            }

            if (e.type == SDL_MOUSEMOTION && e.motion.type == SDL_MOUSEMOTION) {
                x_ = e.motion.x;
                y_ = e.motion.y;

                if (right_) {
                    mat_ = SkMatrix::Translate(-diffx_, -diffy_) * mat_;
                    diffx_ = x_ - downRX_;
                    diffy_ = y_ - downRY_;
                    mat_ = SkMatrix::Translate(diffx_, diffy_) * mat_;
                }
                UpdateInvertMatrix();

                click_ = false;
                auto point = invmat_.mapXY(x_, y_);
                for (const auto &control : controls) {
                    control->OnMouseMotion(point.x(), point.y());
                }
            }

            if (e.type == SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_LEFT) {
                left_ = false;
                if (click_) {
                    auto point = invmat_.mapXY(clickx_, clicky_);
                    for (const auto &control : controls) {
                        if (control->IsContain(point.x(), point.y()) == false) {
                            continue;
                        }

                        control->OnClick(point.x(), point.y());
                    }
                }

                for (const auto &control : controls) {
                    control->OnMouseUp();
                }
            }

            if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT) {
                left_ = true;
                click_ = true;
                clickx_ = e.button.x;
                clicky_ = e.button.y;

                for (const auto &control : controls) {
                    auto point = invmat_.mapXY(clickx_, clicky_);
                    control->OnMouseDown(point.x(), point.y());
                }
            }

            if (e.type == SDL_MOUSEBUTTONUP && e.button.button == SDL_BUTTON_RIGHT) {
                right_ = false;
                mat_ = SkMatrix::Translate(-diffx_, -diffy_) * mat_;
                mat_ = SkMatrix::Translate(e.button.x - downRX_, e.button.y - downRY_) * mat_;
                UpdateInvertMatrix();
                diffx_ = diffy_ = 0;
            }

            if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_RIGHT) {
                right_ = true;
                downRX_ = e.button.x;
                downRY_ = e.button.y;
            }

            if (e.type == SDL_MOUSEWHEEL) {
                if (SDL_GetModState() & KMOD_LCTRL) {
                    auto point = invmat_.mapXY(x_, y_);
                    mat_ = mat_ * SkMatrix::Translate(+point.x(), +point.y());
                    if (e.wheel.y > 0) {
                        mat_ = mat_ * SkMatrix::Scale(1.2, 1.2);
                    } else {
                        mat_ = mat_ * SkMatrix::Scale(1 / 1.2, 1 / 1.2);
                    }
                    mat_ = mat_ * SkMatrix::Translate(-point.x(), -point.y());
                    UpdateInvertMatrix();
                } else {
                    if (e.wheel.y > 0) {
                        mat_ = SkMatrix::Translate(0, 200) * mat_;
                    } else {
                        mat_ = SkMatrix::Translate(0, -200) * mat_;
                    }
                    UpdateInvertMatrix();
                }
            }

            if (e.type == SDL_KEYDOWN) {
                if (e.key.keysym.sym == SDLK_PAGEUP) {
                    mat_ = SkMatrix::Translate(0, 500) * mat_;
                    UpdateInvertMatrix();
                }

                if (e.key.keysym.sym == SDLK_PAGEDOWN) {
                    mat_ = SkMatrix::Translate(0, -500) * mat_;
                    UpdateInvertMatrix();
                }
            }

            if (e.type == SDL_KEYUP && e.key.keysym.sym == SDLK_ESCAPE) {
                exit(0);
            }
        }
    }
}

void SkiaFramework::UpdateInvertMatrix()
{
    if (auto ret = mat_.invert(&invmat_); ret == false) {
        invmat_ = SkMatrix::I();
    }
}

void SkiaFramework::DrawString(SkCanvas &canvas, const std::string &str, double x, double y)
{
    SkPaint textPaint;
    textPaint.setAntiAlias(true);
    textPaint.setColor(0xff0066ff);
    textPaint.setStyle(SkPaint::kFill_Style);

    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);
    canvas.drawString(str.c_str(), x, y, font, textPaint);
}

SkPoint3 SkiaFramework::MeasureString(const std::string &str)
{
    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);
    auto width = font.measureText(str.data(), str.length(), SkTextEncoding::kUTF8);
    SkFontMetrics metrics;
    font.getMetrics(&metrics);
    return {width, -metrics.fAscent + metrics.fDescent, -metrics.fAscent};
}

void SkiaFramework::AddControl(const std::shared_ptr<ISkiaControl> &control)
{
    controls.push_back(control);
}

void SkiaFramework::UIThreadMain()
{
    auto &data = *reinterpret_cast<struct SDLData *>(data_);
    while (uiThreadRunning_) {
        if (data.surface_) {
            SDL_FreeSurface(data.surface_);
            data.surface_ = nullptr;
        }

        if (data.texture_) {
            SDL_DestroyTexture(data.texture_);
            data.texture_ = nullptr;
        }

        {
            std::lock_guard<std::mutex> lock(propsMutex_);
            SkBitmap bitmap;
            bitmap.setInfo(SkImageInfo::Make(data.rect_.w, data.rect_.h,
                                             kBGRA_8888_SkColorType,
                                             kOpaque_SkAlphaType));
            bitmap.allocPixels();
            SkCanvas canvas(bitmap);
            canvas.save();
            canvas.clear(SK_ColorWHITE);
            canvas.setMatrix(mat_);
            DrawBefore(canvas);
            if (onDraw_) {
                onDraw_(canvas);
            }

            for (const auto &control : controls) {
                control->OnDraw(canvas);
            }

            canvas.restore();
            DrawColorPicker(canvas, bitmap);
            DrawAfter(canvas);
            constexpr uint32_t rmask = 0x00ff0000;
            constexpr uint32_t gmask = 0x0000ff00;
            constexpr uint32_t bmask = 0x000000ff;
            constexpr uint32_t amask = 0xff000000;
            data.surface_ = SDL_CreateRGBSurfaceFrom(bitmap.getPixels(),
                                                data.rect_.w, data.rect_.h,
                                                32, data.rect_.w * 4,
                                                rmask, gmask,
                                                bmask, amask);
            data.texture_ = SDL_CreateTextureFromSurface(data.renderer_, data.surface_);
            SDL_RenderCopy(data.renderer_, data.texture_, NULL, &data.rect_);
            SDL_RenderPresent(data.renderer_);
        }
        std::this_thread::sleep_for(16ms);
    }
}

void SkiaFramework::DrawBefore(SkCanvas &canvas)
{
    auto &data = *reinterpret_cast<struct SDLData *>(data_);
    SkPaint paint;
    paint.setAntiAlias(true);
    paint.setColor(0x09000000);
    paint.setStyle(SkPaint::kStroke_Style);
    paint.setStrokeWidth(1);
    SkPaint paint2 = paint;
    paint2.setColor(0x22000000);

    SkPaint textPaint;
    textPaint.setAntiAlias(true);
    textPaint.setColor(0xff00007f);

    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);

    auto rect = invmat_.mapRect(SkRect::MakeXYWH(data.rect_.x, data.rect_.y, data.rect_.w, data.rect_.h));
    auto left = static_cast<int>(rect.left()) / 100 * 100;
    auto right = static_cast<int>(rect.right());
    auto top = static_cast<int>(rect.top()) / 100 * 100;
    auto bottom = static_cast<int>(rect.bottom());

    SkPath path;
    for (int i = left; i <= right; i += 20) {
        path.moveTo(i, -1e9);
        path.lineTo(i, 1e9);
    }

    for (int i = top; i <= bottom; i += 20) {
        path.moveTo(-1e9, i);
        path.lineTo(1e9, i);
    }
    canvas.drawPath(path, paint);

    SkPath path2;
    for (int i = left; i <= right; i += 100) {
        path2.moveTo(i, -1e9);
        path2.lineTo(i, 1e9);

        std::stringstream ss;
        ss << i;
        canvas.drawString(ss.str().c_str(),
                          i,
                          font.getSize() + 0,
                          font, textPaint);
    }

    for (int i = top; i <= bottom; i += 100) {
        path2.moveTo(-1e9, i);
        path2.lineTo(1e9, i);

        std::stringstream ss;
        ss << i;
        canvas.drawString(ss.str().c_str(),
                          0,
                          font.getSize() + i,
                          font, textPaint);
    }
    canvas.drawPath(path2, paint2);
}

void SkiaFramework::DrawColorPicker(SkCanvas &canvas, SkBitmap &bitmap)
{
    if (left_) {
        SkFont font;
        font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
        font.setSize(24);

        SkPaint paint;
        paint.setAntiAlias(true);
        paint.setStyle(SkPaint::kFill_Style);
        SkPaint paint2;
        paint2.setAntiAlias(true);
        paint2.setColor(SK_ColorBLACK);
        paint2.setStyle(paint2.kStroke_Style);

        auto color = bitmap.getColor(x_, y_);
        paint.setColor(color);
        std::stringstream ss;
        ss << std::hex << std::setfill('0') << std::setw(6) << color;
        canvas.drawString(ss.str().c_str(), x_, y_, font, paint2);
        canvas.drawString(ss.str().c_str(), x_, y_, font, paint);
    }
}

void SkiaFramework::DrawAfter(SkCanvas &canvas)
{
    SkPaint paint;
    paint.setAntiAlias(true);
    paint.setColor(0x33000000);
    paint.setStyle(SkPaint::kStroke_Style);
    paint.setStrokeWidth(1);

    SkPaint textPaint;
    textPaint.setAntiAlias(true);
    textPaint.setColor(0xff0000ff);
    textPaint.setStyle(SkPaint::kStroke_Style);
    textPaint.setStrokeWidth(1);

    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);

    SkPath path;
    if (left_) {
        path.moveTo(x_, 0);
        path.lineTo(x_, 1e9);
        path.moveTo(0, y_);
        path.lineTo(1e9, y_);

        auto point = invmat_.mapXY(x_, y_);
        std::stringstream ss;
        ss << "(" << point.x() << ", " << point.y() << ")";
        canvas.drawString(ss.str().c_str(),
                          x_ + 10,
                          font.getSize() + y_ + 10,
                          font, textPaint);
    }

    canvas.drawPath(path, paint);
}
